/*
 * Copyright 2017 - 2024 the original author or authors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see [https://www.gnu.org/licenses/]
 */

package infra.aop.interceptor;

import org.aopalliance.intercept.MethodInvocation;

import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import infra.lang.Assert;
import infra.lang.Nullable;
import infra.logging.Logger;
import infra.util.ClassUtils;
import infra.util.StopWatch;
import infra.util.StringUtils;

/**
 * {@code MethodInterceptor} implementation that allows for highly customizable
 * method-level tracing, using placeholders.
 *
 * <p>Trace messages are written on method entry, and if the method invocation succeeds
 * on method exit. If an invocation results in an exception, then an exception message
 * is written. The contents of these trace messages is fully customizable and special
 * placeholders are available to allow you to include runtime information in your log
 * messages. The placeholders available are:
 *
 * <ul>
 * <li>{@code $[methodName]} - replaced with the name of the method being invoked</li>
 * <li>{@code $[targetClassName]} - replaced with the name of the class that is
 * the target of the invocation</li>
 * <li>{@code $[targetClassShortName]} - replaced with the short name of the class
 * that is the target of the invocation</li>
 * <li>{@code $[returnValue]} - replaced with the value returned by the invocation</li>
 * <li>{@code $[argumentTypes]} - replaced with a comma-separated list of the
 * short class names of the method arguments</li>
 * <li>{@code $[arguments]} - replaced with a comma-separated list of the
 * {@code String} representation of the method arguments</li>
 * <li>{@code $[exception]} - replaced with the {@code String} representation
 * of any {@code Throwable} raised during the invocation</li>
 * <li>{@code $[invocationTime]} - replaced with the time, in milliseconds,
 * taken by the method invocation</li>
 * </ul>
 *
 * <p>There are restrictions on which placeholders can be used in which messages:
 * see the individual message properties for details on the valid placeholders.
 *
 * @author TODAY
 * @author Rob Harrop
 * @author Juergen Hoeller
 * @see #setEnterMessage
 * @see #setExitMessage
 * @see #setExceptionMessage
 * @see SimpleTraceInterceptor
 * @since 3.0
 */
@SuppressWarnings("serial")
public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {

  /**
   * The {@code $[methodName]} placeholder.
   * Replaced with the name of the method being invoked.
   */
  public static final String PLACEHOLDER_METHOD_NAME = "$[methodName]";

  /**
   * The {@code $[targetClassName]} placeholder.
   * Replaced with the fully-qualified name of the {@code Class}
   * of the method invocation target.
   */
  public static final String PLACEHOLDER_TARGET_CLASS_NAME = "$[targetClassName]";

  /**
   * The {@code $[targetClassShortName]} placeholder.
   * Replaced with the short name of the {@code Class} of the
   * method invocation target.
   */
  public static final String PLACEHOLDER_TARGET_CLASS_SHORT_NAME = "$[targetClassShortName]";

  /**
   * The {@code $[returnValue]} placeholder.
   * Replaced with the {@code String} representation of the value
   * returned by the method invocation.
   */
  public static final String PLACEHOLDER_RETURN_VALUE = "$[returnValue]";

  /**
   * The {@code $[argumentTypes]} placeholder.
   * Replaced with a comma-separated list of the argument types for the
   * method invocation. Argument types are written as short class names.
   */
  public static final String PLACEHOLDER_ARGUMENT_TYPES = "$[argumentTypes]";

  /**
   * The {@code $[arguments]} placeholder.
   * Replaced with a comma separated list of the argument values for the
   * method invocation. Relies on the {@code toString()} method of
   * each argument type.
   */
  public static final String PLACEHOLDER_ARGUMENTS = "$[arguments]";

  /**
   * The {@code $[exception]} placeholder.
   * Replaced with the {@code String} representation of any
   * {@code Throwable} raised during method invocation.
   */
  public static final String PLACEHOLDER_EXCEPTION = "$[exception]";

  /**
   * The {@code $[invocationTime]} placeholder.
   * Replaced with the time taken by the invocation (in milliseconds).
   */
  public static final String PLACEHOLDER_INVOCATION_TIME = "$[invocationTime]";

  /**
   * The default message used for writing method entry messages.
   */
  private static final String DEFAULT_ENTER_MESSAGE = "Entering method '" +
          PLACEHOLDER_METHOD_NAME + "' of class [" + PLACEHOLDER_TARGET_CLASS_NAME + "]";

  /**
   * The default message used for writing method exit messages.
   */
  private static final String DEFAULT_EXIT_MESSAGE = "Exiting method '" +
          PLACEHOLDER_METHOD_NAME + "' of class [" + PLACEHOLDER_TARGET_CLASS_NAME + "]";

  /**
   * The default message used for writing exception messages.
   */
  private static final String DEFAULT_EXCEPTION_MESSAGE = "Exception thrown in method '" +
          PLACEHOLDER_METHOD_NAME + "' of class [" + PLACEHOLDER_TARGET_CLASS_NAME + "]";

  /**
   * The {@code Pattern} used to match placeholders.
   */
  private static final Pattern PATTERN = Pattern.compile("\\$\\[\\p{Alpha}+]");

  /**
   * The {@code Set} of allowed placeholders.
   */
  static final Set<String> ALLOWED_PLACEHOLDERS = Set.of(
          PLACEHOLDER_METHOD_NAME,
          PLACEHOLDER_TARGET_CLASS_NAME,
          PLACEHOLDER_TARGET_CLASS_SHORT_NAME,
          PLACEHOLDER_RETURN_VALUE,
          PLACEHOLDER_ARGUMENT_TYPES,
          PLACEHOLDER_ARGUMENTS,
          PLACEHOLDER_EXCEPTION,
          PLACEHOLDER_INVOCATION_TIME
  );

  /**
   * The message for method entry.
   */
  private String enterMessage = DEFAULT_ENTER_MESSAGE;

  /**
   * The message for method exit.
   */
  private String exitMessage = DEFAULT_EXIT_MESSAGE;

  /**
   * The message for exceptions during method execution.
   */
  private String exceptionMessage = DEFAULT_EXCEPTION_MESSAGE;

  /**
   * Set the template used for method entry log messages.
   * This template can contain any of the following placeholders:
   * <ul>
   * <li>{@code $[targetClassName]}</li>
   * <li>{@code $[targetClassShortName]}</li>
   * <li>{@code $[argumentTypes]}</li>
   * <li>{@code $[arguments]}</li>
   * </ul>
   */
  public void setEnterMessage(String enterMessage) throws IllegalArgumentException {
    Assert.hasText(enterMessage, "enterMessage must not be empty");
    checkForInvalidPlaceholders(enterMessage);
    Assert.doesNotContain(enterMessage, PLACEHOLDER_RETURN_VALUE,
            "enterMessage cannot contain placeholder " + PLACEHOLDER_RETURN_VALUE);
    Assert.doesNotContain(enterMessage, PLACEHOLDER_EXCEPTION,
            "enterMessage cannot contain placeholder " + PLACEHOLDER_EXCEPTION);
    Assert.doesNotContain(enterMessage, PLACEHOLDER_INVOCATION_TIME,
            "enterMessage cannot contain placeholder " + PLACEHOLDER_INVOCATION_TIME);
    this.enterMessage = enterMessage;
  }

  /**
   * Set the template used for method exit log messages.
   * This template can contain any of the following placeholders:
   * <ul>
   * <li>{@code $[targetClassName]}</li>
   * <li>{@code $[targetClassShortName]}</li>
   * <li>{@code $[argumentTypes]}</li>
   * <li>{@code $[arguments]}</li>
   * <li>{@code $[returnValue]}</li>
   * <li>{@code $[invocationTime]}</li>
   * </ul>
   */
  public void setExitMessage(String exitMessage) {
    Assert.hasText(exitMessage, "exitMessage must not be empty");
    checkForInvalidPlaceholders(exitMessage);
    Assert.doesNotContain(exitMessage, PLACEHOLDER_EXCEPTION,
            "exitMessage cannot contain placeholder" + PLACEHOLDER_EXCEPTION);
    this.exitMessage = exitMessage;
  }

  /**
   * Set the template used for method exception log messages.
   * This template can contain any of the following placeholders:
   * <ul>
   * <li>{@code $[targetClassName]}</li>
   * <li>{@code $[targetClassShortName]}</li>
   * <li>{@code $[argumentTypes]}</li>
   * <li>{@code $[arguments]}</li>
   * <li>{@code $[exception]}</li>
   * </ul>
   */
  public void setExceptionMessage(String exceptionMessage) {
    Assert.hasText(exceptionMessage, "exceptionMessage must not be empty");
    checkForInvalidPlaceholders(exceptionMessage);
    Assert.doesNotContain(exceptionMessage, PLACEHOLDER_RETURN_VALUE,
            "exceptionMessage cannot contain placeholder " + PLACEHOLDER_RETURN_VALUE);
    this.exceptionMessage = exceptionMessage;
  }

  /**
   * Writes a log message before the invocation based on the value of {@code enterMessage}.
   * If the invocation succeeds, then a log message is written on exit based on the value
   * {@code exitMessage}. If an exception occurs during invocation, then a message is
   * written based on the value of {@code exceptionMessage}.
   *
   * @see #setEnterMessage
   * @see #setExitMessage
   * @see #setExceptionMessage
   */
  @Override
  protected Object invokeUnderTrace(MethodInvocation invocation, Logger logger) throws Throwable {
    String name = ClassUtils.getQualifiedMethodName(invocation.getMethod());
    StopWatch stopWatch = new StopWatch(name);
    Object returnValue = null;
    boolean exitThroughException = false;
    try {
      stopWatch.start(name);
      writeToLog(logger, replacePlaceholders(this.enterMessage,
              invocation, null, null, -1));
      returnValue = invocation.proceed();
      return returnValue;
    }
    catch (Throwable ex) {
      if (stopWatch.isRunning()) {
        stopWatch.stop();
      }
      exitThroughException = true;
      writeToLog(logger, replacePlaceholders(this.exceptionMessage,
              invocation, null, ex, stopWatch.getTotalTimeMillis()), ex);
      throw ex;
    }
    finally {
      if (!exitThroughException) {
        if (stopWatch.isRunning()) {
          stopWatch.stop();
        }
        writeToLog(logger, replacePlaceholders(this.exitMessage,
                invocation, returnValue, null, stopWatch.getTotalTimeMillis()));
      }
    }
  }

  /**
   * Replace the placeholders in the given message with the supplied values,
   * or values derived from those supplied.
   *
   * @param message the message template containing the placeholders to be replaced
   * @param methodInvocation the {@code MethodInvocation} being logged.
   * Used to derive values for all placeholders except {@code $[exception]}
   * and {@code $[returnValue]}.
   * @param returnValue any value returned by the invocation.
   * Used to replace the {@code $[returnValue]} placeholder. May be {@code null}.
   * @param throwable any {@code Throwable} raised during the invocation.
   * The value of {@code Throwable.toString()} is replaced for the
   * {@code $[exception]} placeholder. May be {@code null}.
   * @param invocationTime the value to write in place of the
   * {@code $[invocationTime]} placeholder
   * @return the formatted output to write to the log
   */
  protected String replacePlaceholders(String message, MethodInvocation methodInvocation,
          @Nullable Object returnValue, @Nullable Throwable throwable, long invocationTime) {

    Object target = methodInvocation.getThis();
    Assert.state(target != null, "Target is required");

    StringBuilder output = new StringBuilder();
    Matcher matcher = PATTERN.matcher(message);
    while (matcher.find()) {
      String match = matcher.group();
      switch (match) {
        case PLACEHOLDER_METHOD_NAME -> matcher.appendReplacement(output,
                Matcher.quoteReplacement(methodInvocation.getMethod().getName()));
        case PLACEHOLDER_TARGET_CLASS_NAME -> {
          String className = getClassForLogging(target).getName();
          matcher.appendReplacement(output, Matcher.quoteReplacement(className));
        }
        case PLACEHOLDER_TARGET_CLASS_SHORT_NAME -> {
          String shortName = ClassUtils.getShortName(getClassForLogging(target));
          matcher.appendReplacement(output, Matcher.quoteReplacement(shortName));
        }
        case PLACEHOLDER_ARGUMENTS -> matcher.appendReplacement(output,
                Matcher.quoteReplacement(StringUtils.arrayToCommaDelimitedString(methodInvocation.getArguments())));
        case PLACEHOLDER_ARGUMENT_TYPES -> appendArgumentTypes(methodInvocation, matcher, output);
        case PLACEHOLDER_RETURN_VALUE -> appendReturnValue(methodInvocation, matcher, output, returnValue);
        case PLACEHOLDER_EXCEPTION -> {
          if (throwable != null) {
            matcher.appendReplacement(output, Matcher.quoteReplacement(throwable.toString()));
          }
        }
        case PLACEHOLDER_INVOCATION_TIME -> matcher.appendReplacement(output, Long.toString(invocationTime));
        default -> {
          // Should not happen since placeholders are checked earlier.
          throw new IllegalArgumentException("Unknown placeholder [" + match + "]");
        }
      }
    }
    matcher.appendTail(output);

    return output.toString();
  }

  /**
   * Adds the {@code String} representation of the method return value
   * to the supplied {@code StringBuilder}. Correctly handles
   * {@code null} and {@code void} results.
   *
   * @param methodInvocation the {@code MethodInvocation} that returned the value
   * @param matcher the {@code Matcher} containing the matched placeholder
   * @param output the {@code StringBuilder} to write output to
   * @param returnValue the value returned by the method invocation.
   */
  private static void appendReturnValue(
          MethodInvocation methodInvocation, Matcher matcher, StringBuilder output, @Nullable Object returnValue) {

    if (methodInvocation.getMethod().getReturnType() == void.class) {
      matcher.appendReplacement(output, "void");
    }
    else if (returnValue == null) {
      matcher.appendReplacement(output, "null");
    }
    else {
      matcher.appendReplacement(output, Matcher.quoteReplacement(returnValue.toString()));
    }
  }

  /**
   * Adds a comma-separated list of the short {@code Class} names of the
   * method argument types to the output. For example, if a method has signature
   * {@code put(java.lang.String, java.lang.Object)} then the value returned
   * will be {@code String, Object}.
   *
   * @param methodInvocation the {@code MethodInvocation} being logged.
   * Arguments will be retrieved from the corresponding {@code Method}.
   * @param matcher the {@code Matcher} containing the state of the output
   * @param output the {@code StringBuilder} containing the output
   */
  private static void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuilder output) {
    Class<?>[] argumentTypes = methodInvocation.getMethod().getParameterTypes();
    String[] argumentTypeShortNames = new String[argumentTypes.length];
    for (int i = 0; i < argumentTypeShortNames.length; i++) {
      argumentTypeShortNames[i] = ClassUtils.getShortName(argumentTypes[i]);
    }
    matcher.appendReplacement(output,
            Matcher.quoteReplacement(StringUtils.arrayToCommaDelimitedString(argumentTypeShortNames)));
  }

  /**
   * Checks to see if the supplied {@code String} has any placeholders
   * that are not specified as constants on this class and throws an
   * {@code IllegalArgumentException} if so.
   */
  private static void checkForInvalidPlaceholders(String message) throws IllegalArgumentException {
    Matcher matcher = PATTERN.matcher(message);
    while (matcher.find()) {
      String match = matcher.group();
      if (!ALLOWED_PLACEHOLDERS.contains(match)) {
        throw new IllegalArgumentException("Placeholder [" + match + "] is not valid");
      }
    }
  }

}
